package com.nowcoder.community.util;

import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

/**
 * 对敏感词进行过滤并替换
 */
@Component
public class SensitiveFilter {

    private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);

    //敏感词要替换的字符
    private static final String REPLACEMENT = "***";

    //初始化前缀树的根结点（不存放任何信息）
    private TreeNode root = new TreeNode();

    /*
    PostConstruct 注解用于需要在依赖注入完成后执行任何初始化的方法。这个方法必须在类投入使用之前调用。
    所有支持依赖注入的类都必须支持这个注释。即使类没有请求注入任何资源，也必须调用带有 PostConstruct 注
    释的方法。此注解只能注解一个方法。
    */
    @PostConstruct
    public void init(){

        try (
            InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
            //将字节流转化为字符流
            BufferedReader reader = new BufferedReader(new InputStreamReader(resourceAsStream))
        ) {
            String keyword;
            //这里说明存放敏感词的文件中每行只有一个敏感词
            while ((keyword = reader.readLine()) != null){
                //将读取到的敏感词添加到前缀树中
                this.addKeyword(keyword);
            }
        }catch (Exception e){
            logger.error("加载敏感词文件失败：" + e);
        }
    }

    //此方法是将敏感词添加到前缀树中（前缀树的每一个分支就是一个敏感词）
    private void addKeyword(String keyword){
        TreeNode tempNode = root;

        for (int i = 0; i < keyword.length(); i++) {
            char c = keyword.charAt(i);

            TreeNode subNode = tempNode.getSubNode(c);

            if (subNode == null){
                //初始化下一级结点
                subNode = new TreeNode();
                tempNode.addSubNode(c, subNode);
            }

            //指向子节点，进行下一轮循环
            tempNode = subNode;

            //设置结束标识
            if (i == keyword.length() - 1){
                tempNode.setKeywordEnd(true);
            }
        }
    }


    /**
     * 替换敏感词
     * @param text 替换前的文本
     * @return 替换后的文本
     */
    public String filterSensitiveWords(String text){
        if (StringUtils.isBlank(text)){
            return null;
        }

        //指针1
        TreeNode tempNode = root;

        //指针2
        int begin = 0;

        //指针3
        int position = 0;

        //过滤后的结果放到stringBuilder中
        StringBuilder stringBuilder = new StringBuilder();

        //这个position指针并不一定是递增的，但一定会走到字符的尾部
        while (position < text.length()){
            char c = text.charAt(position);

            //跳过不合法的符号（不必替换这样的符号）
            if (isSymbol(c)){
                //如果指针1处于根结点，将此符号计入结果，让指针2向下走一步
                if (tempNode == root){
                    //向StringBuilder中添加字符c
                    stringBuilder.append(c);
                    begin++;
                }
                //无论符号在开头或中间，指针3都会向下走一步
                position++;
                continue;
            }

            //检查下级节点（是否有敏感词的前缀）
            tempNode = tempNode.getSubNode(c);
            //没有下级节点时，以指针2上的字符为开头的就不是敏感词
            if (tempNode == null){
                //以begin开头的字符串不是敏感词
                stringBuilder.append(text.charAt(begin));

                //进入下一个位置
                position = ++begin;/*************************************************/

                //重新指向根结点
                tempNode = root;
            }else if (tempNode.isKeywordEnd()){//如果旧关键词前缀，同时最终还找到了敏感词的标识，就判定为敏感词
                //替换敏感词，将敏感词替换成***
                stringBuilder.append(REPLACEMENT);

                //进入下一个位置
                begin = ++position;

                //重新指向根结点
                tempNode = root;
            }else{
                //检查下一个字符
                position++;
            }

        }

        //在循环中并没有将最后一批字符记录到stringBuilder中，所以要对这一批字符进行记录
        stringBuilder.append(text.substring(begin));
        return stringBuilder.toString();
    }

    //判断是否为符号
    private boolean isSymbol(Character c){
        /*
        isAsciiAlphanumeric：检查字符是否为 ASCII 7 位数字。
        如果字符的ASCII码是介于 48 和 57 或 65 和 90 或 97 和 122 之间，则返回true

        c < 0x2E80 || c > 0x9FFF : 东亚的文字范围
        */
        return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
    }

    //前缀树，用于存放一些敏感词的信息
    private class TreeNode{

        //关键词结束标识
        private boolean isKeywordEnd = false;

        //子节点（key是子节点字符，value是子节点）
        private Map<Character, TreeNode> sonNode = new HashMap<>();

        //是否访问到了关键词的尾部
        public boolean isKeywordEnd() {
            return isKeywordEnd;
        }

        //设置属性
        public void setKeywordEnd(boolean keywordEnd) {
            isKeywordEnd = keywordEnd;
        }

        //添加子节点
        public void addSubNode(Character c, TreeNode node){
            sonNode.put(c, node);
        }

        //获取子节点
        public TreeNode getSubNode(Character c){
            return sonNode.get(c);
        }

    }
}

/*
首先在bean初始化的时候读取敏感词的文本文件，生成敏感词的前缀树，每一个子节点是敏感词中的字符（用HashMap来存储）。
将输入的文本中的每一个字符在前缀树中进行敏感词过滤（在前缀树中由上向下寻找），如果存在敏感词，就将其替换成***。
*/
